
const pattern = {
  email: /^\S+?@\S+?\.\S+?$/,
  url: new RegExp('^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$', 'i')
}

const FORMAT_MAPPING = {
  int: 'number',
  bool: 'boolean',
  double: 'number',
  long: 'number'
}

function formatMessage (args, resources) {
  const defaultMessage = ['label']
  defaultMessage.forEach((item) => {
    if (args[item] === undefined) {
      args[item] = ''
    }
  })

  let str = resources
  for (const key in args) {
    const reg = new RegExp('{' + key + '}')
    str = str.replace(reg, args[key])
  }
  return str
}

function isEmptyValue (value, type) {
  if (value === undefined || value === null) {
    return true
  }

  if (typeof value === 'string' && !value) {
    return true
  }

  if (Array.isArray(value) && !value.length) {
    return true
  }

  if (type === 'object' && !Object.keys(value).length) {
    return true
  }

  return false
}

class RuleValidator {
  constructor (message) {
    this._message = message
  }

  async validateRule (key, value, data) {
    return new Promise(async (resolve, reject) => {
      const result = await this._invokeValidate(key, value, data)
      if (result == null) {
        resolve(1)
      } else {
        reject(new Error(result))
      }
    })
  }

  async _invokeValidate (key, value, data) {
    let result = null

    const rules = key.rules

    const hasRequired = rules.findIndex((item) => { return item.required })
    if (hasRequired < 0) {
      if (value === null || value === undefined) {
        return result
      }
      if (typeof value === 'string' && !value.length) {
        return result
      }
    }

    const message = this._message

    if (rules === undefined) {
      return message.default
    }

    for (let i = 0; i < rules.length; i++) {
      const rule = rules[i]
      const vt = this._getValidateType(rule)

      if (key.label !== undefined) {
        Object.assign(rule, { label: key.label })
      }

      if (RuleValidatorHelper[vt]) {
        result = RuleValidatorHelper[vt](rule, value, message)
        if (result != null) {
          break
        }
      }

      if (rule.validateFunction) {
        result = await this._validateFunction(rule, value, data, vt)
        if (result != null) {
          break
        }
      }
    }

    return result
  }

  async _validateFunction (rule, value, data, vt) {
    let result = null
    const isAsync = Object.prototype.toString.call(rule.validateFunction).includes('AsyncFunction')
    if (isAsync) {
      try {
        const p = await rule.validateFunction(rule, value, data)
        if (p) {
          result = this._getMessage(rule, p, vt)
        }
      } catch (e) {
        result = this._getMessage(rule, e.message, vt)
      }
    } else {
      let callback = null
      let callbackMessage = null
      let res = rule.validateFunction(rule, value, data, (message) => {
        callback = message
      })
      if (callback) {
        res = !(callback instanceof Error)
        callbackMessage = res ? callback : callback.message
      }
      if (!res) {
        result = this._getMessage(rule, callbackMessage, vt)
      }
    }
    return result
  }

  _getMessage (rule, message, vt) {
    return formatMessage(rule, message || rule.errorMessage || this._message[vt] || message.default)
  }

  _getValidateType (rule) {
    // TODO
    let result = ''
    if (rule.required) {
      result = 'required'
    } else if (rule.enum) {
      result = 'range'
    } else if (rule.maximum || rule.minimum) {
      result = 'rangeNumber'
    } else if (rule.maxLength || rule.minLength) {
      result = 'rangeString'
    } else if (rule.format) {
      result = 'format'
    } else if (rule.pattern) {
      result = 'pattern'
    } else if (rule.validate) {
      result = 'validate'
    }
    return result
  }
}

const RuleValidatorHelper = {
  required (rule, value, message) {
    if (rule.required && isEmptyValue(value, rule.format || typeof value)) {
      return formatMessage(rule, rule.errorMessage || message.required)
    }

    return null
  },

  range (rule, value, message) {
    if (Array.isArray(value)) {
      return formatMessage(rule, rule.errorMessage || message.pattern.mismatch)
    }

    if (rule.enum.indexOf(value) < 0) {
      return formatMessage(rule, message.enum)
    }

    return null
  },

  rangeNumber (rule, value, message) {
    // let { minimum, maximum, exclusiveMinimum, exclusiveMaximum } = rule;
    const min = rule.minimum
    const max = rule.maximum
    const exclusiveMin = rule.exclusiveMinimum
    const exclusiveMax = rule.exclusiveMaximum
    const val = value

    if (!types.number(val)) {
      return formatMessage(rule, rule.errorMessage || message.pattern.mismatch)
    }

    const _min = exclusiveMin ? val <= min : val < min
    const _max = exclusiveMax ? val >= max : val > max

    if (min !== undefined && _min) {
      return formatMessage(rule, rule.errorMessage || message.number.min)
    } else if (max !== undefined && _max) {
      return formatMessage(rule, rule.errorMessage || message.number.max)
    } else if (min !== undefined && max !== undefined && (_min || _max)) {
      return formatMessage(rule, rule.errorMessage || message.number.range)
    }

    return null
  },

  rangeString (rule, value, message) {
    const min = rule.minLength
    const max = rule.maxLength
    const val = value.length

    if (typeof value !== 'string') {
      return formatMessage(rule, rule.errorMessage || message.pattern.mismatch)
    }

    if (min !== undefined && val < min) {
      return formatMessage(rule, rule.errorMessage || message.string.min)
    } else if (max !== undefined && val > max) {
      return formatMessage(rule, rule.errorMessage || message.string.max)
    } else if (min !== undefined && max !== undefined && (val < min || val > max)) {
      return formatMessage(rule, rule.errorMessage || message.string.range)
    }

    return null
  },

  pattern (rule, value, message) {
    if (!types.pattern(rule.pattern, value)) {
      return formatMessage(rule, rule.errorMessage || message.pattern.mismatch)
    }

    return null
  },

  format (rule, value, message) {
    const customTypes = Object.keys(types)
    const format = FORMAT_MAPPING[rule.format] ? FORMAT_MAPPING[rule.format] : rule.format

    if (customTypes.indexOf(format) > -1) {
      if (!types[format](value)) {
        return formatMessage(rule, rule.errorMessage || message.types[format])
      }
    }

    return null
  }
}

const types = {
  integer (value) {
    return types.number(value) && parseInt(value, 10) === value
  },
  string (value) {
    return typeof value === 'string'
  },
  number (value) {
    if (isNaN(value)) {
      return false
    }
    return typeof value === 'number'
  },
  boolean: function (value) {
    return typeof value === 'boolean'
  },
  float: function (value) {
    return types.number(value) && !types.integer(value)
  },
  array (value) {
    return Array.isArray(value)
  },
  object (value) {
    return typeof value === 'object' && !types.array(value)
  },
  date (value) {
    let v
    if (value instanceof Date) {
      v = value
    } else {
      v = new Date(value)
    }
    return typeof v.getTime === 'function' && typeof v.getMonth === 'function' && typeof v.getYear === 'function' && !isNaN(v.getTime())
  },
  timestamp (value) {
    if (!this.integer(value) || Math.abs(value).toString().length > 16) {
      return false
    }

    return this.date(value)
  },
  email (value) {
    return typeof value === 'string' && !!value.match(pattern.email) && value.length < 255
  },
  url (value) {
    return typeof value === 'string' && !!value.match(pattern.url)
  },
  pattern (reg, value) {
    try {
      return new RegExp(reg).test(value)
    } catch (e) {
      return false
    }
  },
  method (value) {
    return typeof value === 'function'
  }
}

class SchemaValidator extends RuleValidator {
  constructor (schema, options) {
    super(SchemaValidator.message)

    this._schema = schema
    this._options = options || null
  }

  updateSchema (schema) {
    this._schema = schema
  }

  async validate (data) {
    const checkResult = this._checkField(data)
    if (checkResult) {
      return checkResult
    }

    const result = await this.invokeValidate(data, false)
    return result.length ? result[0] : null
  }

  async validateAll (data) {
    const checkResult = this._checkField(data)
    if (checkResult) {
      return checkResult
    }

    return await this.invokeValidate(data, true)
  }

  async validateUpdate (data) {
    const checkResult = this._checkField(data)
    if (checkResult) {
      return checkResult
    }

    const result = await this.invokeValidateUpdate(data, false)
    return result.length ? result[0] : null
  }

  async invokeValidate (data, all) {
    const result = []
    const schema = this._schema
    for (const key in schema) {
      const value = schema[key]

      try {
        await this.validateRule(value, data[key], data)
      } catch (error) {
        result.push({
          key: key,
          errorMessage: error.message
        })
        if (!all) break
      }
    }
    return result
  }

  async invokeValidateUpdate (data, all) {
    const result = []
    for (const key in data) {
      try {
        await this.validateRule(this._schema[key], data[key], data)
      } catch (error) {
        result.push({
          key: key,
          errorMessage: error.message
        })
        if (!all) break
      }
    }
    return result
  }

  _checkField (data) {
    const keys = Object.keys(data)
    const keys2 = Object.keys(this._schema)
    if (new Set(keys.concat(keys2)).size === keys2.length) {
      return ''
    }
    return [{ key: 'invalid', errorMessage: SchemaValidator.message.defaultInvalid }]
  }
}

function Message () {
  return {
    default: '验证错误',
    defaultInvalid: '非法字段',
    required: '{label}必填',
    enum: '{label}超出范围',
    whitespace: '{label}不能为空',
    date: {
      format: '{label}日期{value}格式无效',
      parse: '{label}日期无法解析,{value}无效',
      invalid: '{label}日期{value}无效'
    },
    types: {
      string: '{label}类型无效',
      array: '{label}类型无效',
      object: '{label}类型无效',
      number: '{label}类型无效',
      date: '{label}类型无效',
      boolean: '{label}类型无效',
      integer: '{label}类型无效',
      float: '{label}类型无效',
      regexp: '{label}无效',
      email: '{label}类型无效',
      url: '{label}类型无效'
    },
    string: {
      len: '{label}必须为{length}个字符',
      min: '{label}不能少于{minLength}个字符',
      max: '{label}不能超过{maxLength}个字符',
      range: '{label}必须介于{minLength}和{maxLength}个字符之间'
    },
    number: {
      len: '{label}必须等于{length}',
      min: '{label}不能小于{minimum}',
      max: '{label}不能大于{maximum}',
      range: '{label}必须介于{minimum}and{maximum}之间'
    },
    pattern: {
      mismatch: '{label}格式不匹配'
    }
  }
}

SchemaValidator.message = new Message()

export default SchemaValidator
